﻿using NPOI.SS.Formula.Functions;
using NPOI.SS.UserModel;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;

namespace Smart.Editor
{
    public class ExcelConvert
    {
        const int HeadColoum = 9;

        const int INT32 = 1;
        const int STRING = 2;
        const int BOOLEAN = 3;
        const int FLOAT = 4;
        const int PAIR = 5;

        static Regex ms_proto_head_reg = new Regex(@"(^\s*required\s*$|^\s*repeated\s*$)", RegexOptions.Singleline);
        static Regex ms_proto_var_type_reg = new Regex(@"(sint32|string|bool|float|pair)", RegexOptions.Singleline);
        static Regex ms_proto_var_name_reg = new Regex(@"(^\s*[a-zA-Z_][a-zA-Z_0-9]*\s*$|^\s*[a-zA-Z_][a-zA-Z_0-9]*:[0-9]*\s*$)", RegexOptions.Singleline);
        static Dictionary<string, int> ms_typename_to_id = new Dictionary<string, int>
        {
            {"sint32",INT32},
            {"string",STRING},
            {"bool",BOOLEAN},
            {"float",FLOAT},
            {"pair",PAIR},
        };
        static Dictionary<string, string> ms_typename_to_declareTypeName = new Dictionary<string, string>
        {
            {"sint32","int"},
            {"string","string"},
            {"bool","int"},
            {"float","int"},
            {"pair","long"},
        };

        public void LoadFile(string path,int mode)
        {
            Excel excel = new Excel(path);
            excel.Read((string sheetName, string[,] datas)=>
            {
                OnRowLoaded(sheetName, datas, mode);
            });
            excel.Close();
        }

        public void OnRowLoaded(string sheetName,string[,] datas,int mode)
        {
            if(string.IsNullOrEmpty(sheetName))
            {
                Smart.Model.Logger.LogError($"load table failed sheetName is empty !");
                return;
            }

            if(datas.Length < HeadColoum)
            {
                Smart.Model.Logger.LogError($"row numbers can not less than {HeadColoum}");
                return;
            }

            int maxColoums = datas.GetLength(1);

            for (int i = 0; i < HeadColoum; ++i)
            {
                if (!HandleHead(datas, i))
                {
                    return;
                }
            }

            GenerateCSFile(sheetName, datas, mode);

            GenerateAssets(sheetName, datas, mode);
        }

        public bool HandleHead(string[,] heads,int index)
        {
            int maxColoums = heads.GetLength(1);
            if (index == 0)
            {
                for(int j = 0;j < maxColoums;++j)
                {
                    if (!ms_proto_head_reg.IsMatch(heads[index,j]))
                    {
                        Smart.Model.Logger.Log($"the content of cell in first row can only be [required|repeated]");
                        return false;
                    }
                }

                return true;
            }

            if (index == 1)
            {
                for (int j = 0; j < maxColoums; ++j)
                {
                    if (!ms_proto_var_type_reg.IsMatch(heads[index, j]))
                    {
                        Smart.Model.Logger.Log($"the content of cell in second row can only be [sint32|string|enum|bool|float|pair]");
                        return false;
                    }
                }

                return true;
            }

            if (index == 2)
            {
                for (int j = 0; j < maxColoums; ++j)
                {
                    if (!ms_proto_var_name_reg.IsMatch(heads[index,j]))
                    {
                        Smart.Model.Logger.Log($"var name can only be start with letter and flowed with [a-z|A-Z|0-9|_]");
                        return false;
                    }
                }

                return true;
            }

            return true;
        }

        void WriteContentValue(int typeId, string content, bool isArray)
        {
            switch (typeId)
            {
                case INT32:
                    {
                        int value = 0;
                        int.TryParse(content, out value);
                        if (isArray)
                            this.varIntValues.Add(value);
                        else
                            this.intValues.Add(value);
                    }
                    break;
                case STRING:
                    {
                        this.varStringValues.Add(content);
                    }
                    break;
                case BOOLEAN:
                    {
                        int value = 0;
                        int.TryParse(content, out value);
                        value = value == 0 ? 0 : 1;
                        if (isArray)
                            this.varIntValues.Add(value);
                        else
                            this.intValues.Add(value);
                    }
                    break;
                case FLOAT:
                    {
                        int value = 0;
                        int.TryParse(content, out value);
                        if (isArray)
                            this.varIntValues.Add(value);
                        else
                            this.intValues.Add(value);
                    }
                    break;
                case PAIR:
                    {
                        int key = 0;
                        int value = 0;
                        long kv = 0L;
                        var tokens = content.Split('#');
                        if(tokens.Length == 2 && int.TryParse(tokens[0],out key) && int.TryParse(tokens[1],out value))
                        {
                            kv = ((long)key) | (((long)value) << 32);
                        }
                        if (isArray)
                            this.varLongValues.Add(kv);
                        else
                            this.longValues.Add(kv);
                    }
                    break;
            }
        }

        void WriteContent(Stream stream,string content,bool isArray,int typeId)
        {
            if(!isArray && typeId != STRING)
            {
                WriteContentValue(typeId, content, isArray);
                return;
            }

            if (string.IsNullOrEmpty(content))
                content = string.Empty;

            var tokens = content.Split(typeId == PAIR ? '&' : '#');
            int offset = 0;
            int typeKey = 0;
            if (typeId == STRING)
            {
                offset = varStringValues.Count;
                typeKey = 2;
            }
            else if(typeId == PAIR)
            {
                offset = varLongValues.Count;
                typeKey = 3;
            }
            else
            {
                offset = varIntValues.Count;
                typeKey = 1;
            }

            if(!offsetDic.TryGetValue(typeKey,out Dictionary<string,int> kvs))
            {
                kvs = new Dictionary<string, int>();
                offsetDic.Add(typeKey, kvs);
            }

            bool existed = kvs.ContainsKey(content);
            if(existed)
            {
                this.intValues.Add(kvs[content]);
                return;
            }

            int length = tokens.Length;
            int arrValue = ((length & 0xFFF) << 20) | (offset & 0xFFFFF);

            this.intValues.Add(arrValue);

            for (int j = 0; j < tokens.Length;++j)
            {
                WriteContentValue(typeId, tokens[j],isArray);
            }

            kvs.Add(content, arrValue);
        }

        void GetDeclareContent(StringBuilder stringBuilder,int typeId,string declareTypeName,string varName,bool isArray,int count)
        {
            if (typeId == PAIR)
            {
                if (!isArray)
                {
                    stringBuilder.AppendLine($"\t\tpublic {declareTypeName} {varName}");
                    stringBuilder.AppendLine($"\t\t{{");
                    stringBuilder.AppendLine($"\t\t\tget");
                    stringBuilder.AppendLine($"\t\t\t{{");
                    stringBuilder.AppendLine($"\t\t\t\treturn __data.longValues[handle.longOffset + {count}];");
                    stringBuilder.AppendLine($"\t\t\t}}");
                    stringBuilder.AppendLine($"\t\t}}");
                }
                else
                {
                    stringBuilder.AppendLine($"\t\tpublic int {varName}Ptr");
                    stringBuilder.AppendLine($"\t\t{{");
                    stringBuilder.AppendLine($"\t\t\tget");
                    stringBuilder.AppendLine($"\t\t\t{{");
                    stringBuilder.AppendLine($"\t\t\t\treturn __data.intValues[handle.intOffset + {count}];");
                    stringBuilder.AppendLine($"\t\t\t}}");
                    stringBuilder.AppendLine($"\t\t}}");

                    stringBuilder.AppendLine($"\t\tpublic LongArray {varName}");
                    stringBuilder.AppendLine($"\t\t{{");
                    stringBuilder.AppendLine($"\t\t\tget");
                    stringBuilder.AppendLine($"\t\t\t{{");
                    stringBuilder.AppendLine($"\t\t\t\tLongArray array;");
                    stringBuilder.AppendLine($"\t\t\t\tarray.__values = __data.varLongValues;");
                    stringBuilder.AppendLine($"\t\t\t\tarray.__start = {varName}Ptr & 0xFFFFF;");
                    stringBuilder.AppendLine($"\t\t\t\tarray.__length = ({varName}Ptr >> 20) & 0xFFF;");
                    stringBuilder.AppendLine($"\t\t\t\tarray.__end = array.__length + array.__start;");
                    stringBuilder.AppendLine($"\t\t\t\treturn array;");
                    stringBuilder.AppendLine($"\t\t\t}}");
                    stringBuilder.AppendLine($"\t\t}}");
                }
            }
            else if (typeId == STRING)
            {
                if (!isArray)
                {
                    stringBuilder.AppendLine($"\t\tpublic {declareTypeName} {varName}");
                    stringBuilder.AppendLine($"\t\t{{");
                    stringBuilder.AppendLine($"\t\t\tget");
                    stringBuilder.AppendLine($"\t\t\t{{");
                    stringBuilder.AppendLine($"\t\t\t\treturn __data.varStringValues[__data.intValues[handle.intOffset + {count}] & 0xFFFFF];");
                    stringBuilder.AppendLine($"\t\t\t}}");
                    stringBuilder.AppendLine($"\t\t}}");
                }
                else
                {
                    stringBuilder.AppendLine($"\t\tpublic int {varName}Ptr");
                    stringBuilder.AppendLine($"\t\t{{");
                    stringBuilder.AppendLine($"\t\t\tget");
                    stringBuilder.AppendLine($"\t\t\t{{");
                    stringBuilder.AppendLine($"\t\t\t\treturn __data.intValues[handle.intOffset + {count}];");
                    stringBuilder.AppendLine($"\t\t\t}}");
                    stringBuilder.AppendLine($"\t\t}}");

                    stringBuilder.AppendLine($"\t\tpublic StringArray {varName}");
                    stringBuilder.AppendLine($"\t\t{{");
                    stringBuilder.AppendLine($"\t\t\tget");
                    stringBuilder.AppendLine($"\t\t\t{{");
                    stringBuilder.AppendLine($"\t\t\t\tStringArray array;");
                    stringBuilder.AppendLine($"\t\t\t\tarray.__values = __data.varStringValues;");
                    stringBuilder.AppendLine($"\t\t\t\tarray.__start = {varName}Ptr & 0xFFFFF;");
                    stringBuilder.AppendLine($"\t\t\t\tarray.__length = ({varName}Ptr >> 20) & 0xFFF;");
                    stringBuilder.AppendLine($"\t\t\t\tarray.__end = array.__length + array.__start;");
                    stringBuilder.AppendLine($"\t\t\t\treturn array;");
                    stringBuilder.AppendLine($"\t\t\t}}");
                    stringBuilder.AppendLine($"\t\t}}");
                }
            }
            else
            {
                if (!isArray)
                {
                    stringBuilder.AppendLine($"\t\tpublic {declareTypeName} {varName}");
                    stringBuilder.AppendLine($"\t\t{{");
                    stringBuilder.AppendLine($"\t\t\tget");
                    stringBuilder.AppendLine($"\t\t\t{{");
                    stringBuilder.AppendLine($"\t\t\t\treturn __data.intValues[handle.intOffset + {count}];");
                    stringBuilder.AppendLine($"\t\t\t}}");
                    stringBuilder.AppendLine($"\t\t}}");
                }
                else
                {
                    stringBuilder.AppendLine($"\t\tpublic int {varName}Ptr");
                    stringBuilder.AppendLine($"\t\t{{");
                    stringBuilder.AppendLine($"\t\t\tget");
                    stringBuilder.AppendLine($"\t\t\t{{");
                    stringBuilder.AppendLine($"\t\t\t\treturn __data.intValues[handle.intOffset + {count}];");
                    stringBuilder.AppendLine($"\t\t\t}}");
                    stringBuilder.AppendLine($"\t\t}}");

                    stringBuilder.AppendLine($"\t\tpublic IntArray {varName}");
                    stringBuilder.AppendLine($"\t\t{{");
                    stringBuilder.AppendLine($"\t\t\tget");
                    stringBuilder.AppendLine($"\t\t\t{{");
                    stringBuilder.AppendLine($"\t\t\t\tIntArray array;");
                    stringBuilder.AppendLine($"\t\t\t\tarray.__values = __data.varIntValues;");
                    stringBuilder.AppendLine($"\t\t\t\tarray.__start = {varName}Ptr & 0xFFFFF;");
                    stringBuilder.AppendLine($"\t\t\t\tarray.__length = ({varName}Ptr >> 20) & 0xFFF;");
                    stringBuilder.AppendLine($"\t\t\t\tarray.__end = array.__length + array.__start;");
                    stringBuilder.AppendLine($"\t\t\t\treturn array;");
                    stringBuilder.AppendLine($"\t\t\t}}");
                    stringBuilder.AppendLine($"\t\t}}");
                }
            }
        }

        void GenerateCSFile(string sheetName, string[,] datas,int mode)
        {
            int maxColoums = datas.GetLength(1);

            int rowCount = datas.GetLength(0) - HeadColoum;
            if (rowCount < 0)
                rowCount = 0;

            var clsName = sheetName.ToUpper();
            string csFileName = string.Empty;
            if(mode == 0)
            {
                csFileName = $"{Application.dataPath}/Editor/Table/{sheetName}.cs";
            }
            else if(mode == 1)
            {
                csFileName = $"{Application.dataPath}/Scripts/Model/Table/{sheetName}.cs";
            }
            else
            {
                csFileName = $"{Application.dataPath}/Scripts/Hotfix/Table/{sheetName}.cs";
            }
            var csFileDir = System.IO.Path.GetDirectoryName(csFileName);
            StringBuilder stringBuilder = new StringBuilder(1024);
            stringBuilder.AppendLine("using Smart.Model;");
            stringBuilder.AppendLine();
            if(mode == 0)
            {
                stringBuilder.AppendLine("namespace Smart.Editor.TABLE");
            }
            else if(mode == 1)
            {
                stringBuilder.AppendLine("namespace Smart.Model.TABLE");
            }
            else
            {
                stringBuilder.AppendLine("namespace Smart.Hotfix.TABLE");
            }

            stringBuilder.AppendLine("{");
            if (mode != 0)
            {
                stringBuilder.AppendLine($"\t[TableLoader]");
            }
            if(mode == 0)
                stringBuilder.AppendLine($"\tpublic class {clsName.ToUpper()}Extend : Smart.Model.TABLE.ITableLoader");
            else
                stringBuilder.AppendLine($"\tpublic class {clsName.ToUpper()}Extend : ITableLoader");
            stringBuilder.AppendLine("\t{");
            stringBuilder.AppendLine($"\t\tpublic string FileName => \"{clsName.ToUpper()}\";");
            stringBuilder.AppendLine("\t\tpublic void Init(TableData tableData)");
            stringBuilder.AppendLine("\t\t{");
            stringBuilder.AppendLine($"\t\t\t{clsName.ToUpper()}.__data = tableData;");
            stringBuilder.AppendLine("\t\t}");

            stringBuilder.AppendLine($"\t\tpublic object CreateAndBind(TableHandle handle)");
            stringBuilder.AppendLine("\t\t{");
            stringBuilder.AppendLine($"\t\t\tvar item = new {clsName.ToUpper()}();");
            stringBuilder.AppendLine($"\t\t\titem.handle = handle;");
            stringBuilder.AppendLine($"\t\t\treturn item;");
            stringBuilder.AppendLine("\t\t}");

            stringBuilder.AppendLine($"\t\tpublic void Bind(object item, TableHandle handle)");
            stringBuilder.AppendLine("\t\t{");
            stringBuilder.AppendLine($"\t\t\t(item as {clsName.ToUpper()}).handle = handle;");
            stringBuilder.AppendLine("\t\t}");

            stringBuilder.AppendLine("\t}");
            stringBuilder.AppendLine();

            stringBuilder.AppendLine($"\tpublic class {clsName} : ITable");
            stringBuilder.AppendLine("\t{");
            stringBuilder.AppendLine("\t\tpublic TableHandle handle;");
            stringBuilder.AppendLine("\t\tpublic static TableData __data;");

            int[] valueOffsets = new int[3];
            for (int j = 0; j < maxColoums; ++j)
            {
                bool isArray = string.Equals(datas[0, j], "repeated");
                int ruleValue = 0;
                if (isArray)
                    ruleValue |= (1 << 7);

                string varType = datas[1, j];
                int typeId = ms_typename_to_id[varType];
                int count = 0;
                if (typeId == PAIR)
                {
                    ruleValue |= 4;
                    if (!isArray)
                    {
                        //long offset
                        count = valueOffsets[1];
                        valueOffsets[1] += 1;
                    }
                    else
                    {
                        //int offset
                        count = valueOffsets[0];
                        valueOffsets[0] += 1;
                    }
                }
                else if (typeId == STRING)
                {
                    ruleValue |= 2;

                    //int offset
                    count = valueOffsets[0];
                    valueOffsets[0] += 1;
                }
                else
                {
                    //int offset
                    ruleValue |= 1;
                    count = valueOffsets[0];
                    valueOffsets[0] += 1;
                }

                var declareTypeName = ms_typename_to_declareTypeName[varType];
                string varName = datas[2, j];

                GetDeclareContent(stringBuilder, typeId, declareTypeName, varName, isArray, count);
            }

            stringBuilder.AppendLine("\t}");
            stringBuilder.AppendLine("}");

            if (!System.IO.Directory.Exists(csFileDir))
            {
                System.IO.Directory.CreateDirectory(csFileDir);
            }

            if (System.IO.File.Exists(csFileName))
            {
                System.IO.File.Delete(csFileName);
            }

            System.IO.File.WriteAllText(csFileName, stringBuilder.ToString());

            Smart.Model.Logger.Log($"<color=#00ff00>convert {sheetName}.cs succeed !</color>");
        }

        void GetRules(int typeId,bool isArray,ref int intFixedLength,ref int longFixedLength,ref int stringFixedLength)
        {
            if (typeId == PAIR)
            {
                if (!isArray)
                {
                    ++longFixedLength;
                }
                else
                {
                    ++intFixedLength;
                }
            }
            else if (typeId == STRING)
            {
                ++intFixedLength;
            }
            else
            {
                ++intFixedLength;
            }
        }

        List<int> intValues = new List<int>(4096);
        List<long> longValues = new List<long>(4096);
        List<string> stringValues = new List<string>(4096);
        List<int> varIntValues = new List<int>(4096);
        List<long> varLongValues = new List<long>(4096);
        List<string> varStringValues = new List<string>(4096);
        Dictionary<int, Dictionary<string, int>> offsetDic = new Dictionary<int, Dictionary<string, int>>(8);

        void GenerateAssets(string sheetName, string[,] datas,int mode)
        {
            int maxColoums = datas.GetLength(1);

            string dir = string.Empty;
            if(mode == 0)
            {
                dir = System.IO.Path.GetFullPath(Application.dataPath + "/Editor/Res/Table");
            }
            else
            {
                dir = System.IO.Path.GetFullPath(Application.dataPath + "/Res/Table");
            }

            if (!System.IO.Directory.Exists(dir))
                System.IO.Directory.CreateDirectory(dir);

            var fileName = $"{dir}/{sheetName}.bytes";
            if (System.IO.File.Exists(fileName))
                System.IO.File.Delete(fileName);

            using (FileStream fileStream = new FileStream(fileName, FileMode.Create, FileAccess.Write))
            {
                int rowCount = datas.GetLength(0) - HeadColoum;
                if (rowCount < 0)
                    rowCount = 0;

                intValues.Clear();
                longValues.Clear();
                stringValues.Clear();
                varIntValues.Clear();
                varLongValues.Clear();
                varStringValues.Clear();
                int intFixedLength = 0;
                int longFixedLength = 0;
                int stringFixedLength = 0;
                offsetDic.Clear();

                for (int i = HeadColoum, maxi = datas.GetLength(0); i < maxi; ++i)
                {
                    for (int j = 0; j < maxColoums; ++j)
                    {
                        bool isArray = string.Equals(datas[0, j], "repeated");
                        string varType = datas[1, j];
                        int typeId = ms_typename_to_id[varType];

                        //写入规则
                        if(i == HeadColoum)
                        {
                            GetRules(typeId, isArray,ref intFixedLength,ref longFixedLength,ref stringFixedLength);
                        }

                        WriteContent(fileStream, datas[i, j], isArray, typeId);
                    }
                }

                //1 write varValues ...
                fileStream.EncodeIntArray(varIntValues);
                fileStream.EncodeStringArray(varStringValues);
                fileStream.EncodeLongArray(varLongValues);
                //2 write fixed length
                fileStream.Encode(intFixedLength);
                fileStream.Encode(stringFixedLength);
                fileStream.Encode(longFixedLength);
                //3 write rows
                fileStream.Encode(rowCount);
                //4 write fixed Values ...
                fileStream.EncodeIntArray(intValues);
                fileStream.EncodeStringArray(stringValues);
                fileStream.EncodeLongArray(longValues);

                fileStream.Flush();
                fileStream.Close();
            }

            Smart.Model.Logger.Log($"<color=#00ff00>convert {sheetName}.bytes succeed !</color>");
        }
    }
}